// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: tu@tulrich.com (Thatcher Ulrich)

// C++ translation of the WebGL benchmark code written in Javascript.
// Uses the Desktop OpenGL API.

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "webgl_bench.h"
#include "glmath.h"

template<class T>
T min2(T a, T b) {
  return a < b ? a : b;
}

template<class T>
T max2(T a, T b) {
  return a > b ? a : b;
}

void CheckGlError(const char* msg) {
  GLenum error = glGetError();
  if (error != GL_NO_ERROR) {
    fprintf(stderr, "GL Error: %d (%s)\n", error, msg);
    fflush(stderr);
    exit(1);
  }
}

Model::Model() : texture_(0), vertices_(0), indices_(0),
                 index_count_(0), index_type_(0), triangle_count_(0) {
  memset(matrix_, 0, sizeof(matrix_));
}

Model::~Model() {
}

void Model::MakeGridTriangles(int dim, vector<GLshort>* out) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  // 
  // TODO: Investigate alternative vert orderings and slab heights.
  const int SLAB_HEIGHT = 8;
  out->resize((dim - 1) * (dim - 1) * 6);
  int next_i = 0;
  for (int slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (int i = 0; i < dim - 1; i++) {
      int end_j = min2(dim - 1, slab_j + SLAB_HEIGHT);
      for (int j = slab_j; j < end_j; j++) {
        int vi = j * dim + i;
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        (*out)[next_i++] = vi;
        (*out)[next_i++] = vi + dim + 1;
        (*out)[next_i++] = vi + dim;
        (*out)[next_i++] = vi;
        (*out)[next_i++] = vi + 1;
        (*out)[next_i++] = vi + dim + 1;
      }
    }
  }
}

void Model::MakeGridTriangleStrip(int dim, vector<GLshort>* out) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  // 
  // TODO: Investigate alternative vert orderings and slab heights.
  const int SLAB_HEIGHT = 8;
  out->resize(0);
  for (int slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (int i = 0; i < dim - 1; i++) {
      int end_j = min2(dim - 1, slab_j + SLAB_HEIGHT);
      int vi = slab_j * dim + i;
      if (out->size()) {
        // Start the strip with a degenerate if we're connecting to a
        // previous strip.
        out->push_back(vi + 1);
      }
      out->push_back(vi + 1);
      out->push_back(vi);
      for (int j = slab_j; j < end_j; j++) {
        //  *--*
        //  |\ | ...
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //   .
        //   .
        //   .
        out->push_back(vi + dim + 1);
        out->push_back(vi + dim);
        vi += dim;
      }
      // End the strip with a degenerate tri in order to start the
      // next strip.
      out->push_back((*out)[out->size() - 1]);
    }
  }
}

// (*data)[] is both source and destination.
void BoxDownsample(int src_w, int src_h, int dst_w, int dst_h,
                   vector<GLbyte>* data) {
  int shift_x = dst_w * 2 == src_w ? 1 : 0;  // Shrink horizontally.
  int shift_y = dst_h * 2 == src_h ? 1 : 0;  // Shrink vertically.
  int downshift = shift_x + shift_y;

  for (int y = 0; y < dst_h; y++) {
    for (int x = 0; x < dst_w; x++) {
      int dst_idx = (y * dst_w + x) * 4;
      int src0_idx = ((y << shift_y) * src_w + (x << shift_x)) * 4;
      int offset[4] = { 0,
                        shift_x * 4,
                        shift_y * src_w * 4,
                        shift_x * 4 + shift_y * src_w * 4 };
      int c[4] = { 0, 0, 0, 0 };
      for (int srcs = 0; srcs < 4; srcs++) {
        int src_idx = src0_idx + offset[srcs];
        for (int k = 0; k < 4; k++) {
          c[k] += (*data)[src_idx + k];
        }
      }
      for (int k = 0; k < 4; k++) {
        (*data)[dst_idx + k] = c[k] >> downshift;
      }
    }
  }
}

void MyGenerateMipmap(int w, int h, const vector<GLbyte>& imdata) {
  vector<GLbyte> data(imdata);  // Make a working copy.
  int level = 0;
  while (w > 0 && h > 0) {
    level++;
    int w2 = w >> 1;
    int h2 = h >> 1;
    int w2_temp = max2(1, w2);
    int h2_temp = max2(1, h2);
    BoxDownsample(w, h, w2_temp, h2_temp, &data);
    glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, w2_temp, h2_temp, 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, &data[0]);
    w = w2;
    h = h2;
  }
}

void Model::Init(enum PrimType prim_type, int x, int y, int d, int grid_dim) {
  float origin[3] = {d * -0.15f,
                     (x - 2) * 0.5f,
                     (y - 2) * 0.5f};
  float up[3] = {1, 0, 0};
  float dir[3] = {0, 0, 0};
  if (fabs(up[1]) > 0.8) {
    dir[2] = 1;
  } else {
    dir[1] = 1;
  }
  float right[3];
  CrossProduct(dir, up, right);
  float temp[3];
  CrossProduct(up, right, temp);
  VectorNormalize(temp, dir);
 
  // Pick some randomish constants for modulating the model.
  int seed = x + y * 27 + d * 129;
  static const float coeffs[8] = {
    0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8
  };
  const float c0 = coeffs[(seed * 13 + 1) % 8];
  const float c1 = coeffs[(seed * 23 + 2) % 8];
  const float c2 = coeffs[(seed * 7 + 3) % 8];
  const float c3 = coeffs[(seed * 17 + 4) % 8];
  const float c4 = coeffs[(seed * 5 + 5) % 8];
  const float c5 = coeffs[(seed * 3 + 6) % 8];
  const float c6 = coeffs[(seed * 11 + 7) % 8];
  const float c7 = coeffs[(seed * 31 + 0) % 8];
  const float c8 = coeffs[(seed * 43 + 1) % 8];
  const float c9 = coeffs[(seed * 119 + 7) % 8];
  const float c10 = coeffs[(seed * 67 + 2) % 8];
  
  int DIM = grid_dim;
  vector<float> verts(DIM * DIM * 5);
  for (int j = 0; j < DIM; j++) {
    for (int i = 0; i < DIM; i++) {
      // Position.
      float u = i / float(DIM - 1);
      float v = j / float(DIM - 1);
      verts[(j * DIM + i) * 5 + 0] = (u - 0.5) * 0.5;
      verts[(j * DIM + i) * 5 + 1] = (sinf(u * c0 * 10 + c1) + cosf(v * c2 * 20) + c3) * c4 * 0.15;
      verts[(j * DIM + i) * 5 + 2] = (v - 0.5) * 0.5;
 
      // Texture UV.
      verts[(j * DIM + i) * 5 + 3] = u;
      verts[(j * DIM + i) * 5 + 4] = v;
    }
  }
  GLuint verts_buf = 0;
  glGenBuffers(1, &verts_buf);
  glBindBuffer(GL_ARRAY_BUFFER, verts_buf);
  glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(verts[0]), &verts[0], GL_STATIC_DRAW);
 
  CheckGlError("init_model 1");
 
  vector<GLshort> inds_array;
  int index_type;
  if (prim_type == BENCH_TRIANGLES) {
    MakeGridTriangles(DIM, &inds_array);
    index_type = GL_TRIANGLES;
  } else {
    MakeGridTriangleStrip(DIM, &inds_array);
    index_type = GL_TRIANGLE_STRIP;
  }
  GLuint inds_buf = 0;
  glGenBuffers(1, &inds_buf);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, inds_buf);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER,
               inds_array.size() * sizeof(inds_array[0]),
               &inds_array[0],
               GL_STATIC_DRAW);
  int index_count = inds_array.size();
 
  CheckGlError("init_model 2");
 
  // Generate a texture.
  int TEX_DIM = 128;
  vector<GLbyte> imdata(TEX_DIM * TEX_DIM * 4);
  for (int j = 0; j < TEX_DIM; j++) {
    for (int i = 0; i < TEX_DIM; i++) {
      float u = i / float(TEX_DIM - 1);
      float v = j / float(TEX_DIM - 1);
      int vi = (j * TEX_DIM + i) * 4;
      imdata[vi + 0] = fabs(sinf((u * c5 + c8) * 12)) * 255;    // red
      imdata[vi + 1] = fabs(cosf((v * c6 + c9) * 16)) * 255;   // green
      imdata[vi + 2] = cosf(((u + c7) * (v + c8) + c10) * 100) * 127 + 128;   // blue
      imdata[vi + 3] = 255;   // alpha
    }
  }
  CheckGlError("init_model 2");
  GLuint texture = 0;
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                  GL_LINEAR_MIPMAP_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEX_DIM, TEX_DIM, 0, GL_RGBA,
               GL_UNSIGNED_BYTE, &imdata[0]);
  MyGenerateMipmap(TEX_DIM, TEX_DIM, imdata);
  glBindTexture(GL_TEXTURE_2D, 0);
 
  CheckGlError("init_model 3");
 
  float model_mat[16];
  MakeOrientationAffine(origin, dir, up, model_mat);
 
  memcpy(matrix_, model_mat, sizeof(matrix_));
  vertices_ = verts_buf;
  indices_ = inds_buf;
  index_count_ = index_count;
  index_type_ = index_type;
  // GL_LINES -- poor-person's wireframe
  triangle_count_ = (DIM - 1) * (DIM - 1) * 2;
  texture_ = texture;
 
  CheckGlError("init_model post");
}

void Model::Draw(RenderInfo* ri) const {
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, texture_);

  glBindBuffer(GL_ARRAY_BUFFER, vertices_);
  glVertexAttribPointer(ri->position_location, 3, GL_FLOAT, GL_FALSE, 4 * 5, 0);
  glVertexAttribPointer(ri->t0_uv_location, 2, GL_FLOAT, GL_FALSE, 4 * 5, (void*) (4 * 3));
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices_);
  glDrawElements(index_type_, index_count_, GL_UNSIGNED_SHORT, 0);
  ri->tri_count += triangle_count_;
  ri->draw_count += 1;
}

Bench::Bench() {
}

Bench::~Bench() {
}

void Bench::Init(int depth_x, int dim, PrimType prim_type) {
  CheckGlError("init_world pre");
  models_.resize(0);
  for (int d = 0; d < depth_x; d++) {
    for (int j = 0; j < 5; j++) {
      for (int i = 0; i < 5; i++) {
        Model* model = new Model;
        model->Init(prim_type, i, j, d, dim);
        models_.push_back(model);
      }
    }
  }
  CheckGlError("init_world post");
}

void Bench::Draw(RenderInfo* ri) const {
  float view_matrix[16];
  MakeViewFromOrientation(ri->camera_orient, view_matrix);

  float vp[16];
  MyMatrixMultiply(ri->projection_matrix, view_matrix, vp);

  float mvp[16];
 
  for (int i = 0; i < models_.size(); i++) {
    const Model* model = models_[i];
    MyMatrixMultiply(vp, model->matrix(), mvp);
    glUniformMatrix4fv(ri->mvp_location, 1, false, mvp);
    model->Draw(ri);
  }
}
